// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.

package org.jetbrains.kotlin.idea.inspections.collections

import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.util.TextRange
import org.jetbrains.kotlin.builtins.StandardNames
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
import org.jetbrains.kotlin.idea.quickfix.ReplaceSelectorOfQualifiedExpressionFix
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtQualifiedExpression
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.inference.model.TypeVariableTypeConstructor
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.util.getType
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.TypeUtils
import org.jetbrains.kotlin.types.Variance
import org.jetbrains.kotlin.types.isFlexible
import org.jetbrains.kotlin.types.typeUtil.isSubtypeOf

// TODO: This inspection has been ported to K2 with an implementation that will also work for K1,
//  but there is a bug in the analysis API preventing it from being used for both.
//  Once KT-65376 is fixed, remove this class and use the shared implementation instead.
class UselessCallOnCollectionInspection : AbstractUselessCallInspection() {
    override val uselessFqNames = mapOf(
        "kotlin.collections.filterNotNull" to deleteConversion,
        "kotlin.sequences.filterNotNull" to deleteConversion,
        "kotlin.collections.filterIsInstance" to deleteConversion,
        "kotlin.sequences.filterIsInstance" to deleteConversion,
        "kotlin.collections.mapNotNull" to Conversion("map"),
        "kotlin.sequences.mapNotNull" to Conversion("map"),
        "kotlin.collections.mapNotNullTo" to Conversion("mapTo"),
        "kotlin.sequences.mapNotNullTo" to Conversion("mapTo"),
        "kotlin.collections.mapIndexedNotNull" to Conversion("mapIndexed"),
        "kotlin.sequences.mapIndexedNotNull" to Conversion("mapIndexed"),
        "kotlin.collections.mapIndexedNotNullTo" to Conversion("mapIndexedTo"),
        "kotlin.sequences.mapIndexedNotNullTo" to Conversion("mapIndexedTo")
    )

    override val uselessNames = uselessFqNames.keys.toShortNames()

    override fun QualifiedExpressionVisitor.suggestConversionIfNeeded(
        expression: KtQualifiedExpression,
        calleeExpression: KtExpression,
        context: BindingContext,
        conversion: Conversion
    ) {
        val receiverType = expression.receiverExpression.getType(context) ?: return
        val receiverTypeArgument = receiverType.arguments.singleOrNull() ?: return
        val receiverTypeArgumentType = receiverTypeArgument.type
        val resolvedCall = expression.getResolvedCall(context) ?: return
        if (calleeExpression.text == "filterIsInstance") {
            if (receiverTypeArgument.projectionKind == Variance.IN_VARIANCE) return
            val typeParameterDescriptor = resolvedCall.candidateDescriptor.typeParameters.singleOrNull() ?: return
            val argumentType = resolvedCall.typeArguments[typeParameterDescriptor] ?: return
            if (receiverTypeArgumentType.isFlexible() || !receiverTypeArgumentType.isSubtypeOf(argumentType)) return
        } else {
            // xxxNotNull
            if (TypeUtils.isNullableType(receiverTypeArgumentType)) return
            if (calleeExpression.text != "filterNotNull") {
                // Also check last argument functional type to have not-null result
                val lastParameterMatches = resolvedCall.hasLastFunctionalParameterWithResult(context) {
                    !TypeUtils.isNullableType(it) && it.constructor !is TypeVariableTypeConstructor
                }
                if (!lastParameterMatches) return
            }
        }

        val newName = conversion.replacementName
        if (newName != null) {
            val descriptor = holder.manager.createProblemDescriptor(
                expression,
                TextRange(
                    expression.operationTokenNode.startOffset - expression.startOffset,
                    calleeExpression.endOffset - expression.startOffset
                ),
                KotlinBundle.message("call.on.collection.type.may.be.reduced"),
                ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                isOnTheFly,
                RenameUselessCallFix(newName)
            )
            holder.registerProblem(descriptor)
        } else {
            val fix = if (resolvedCall.resultingDescriptor.returnType.isList() && !receiverType.isList()) {
                ReplaceSelectorOfQualifiedExpressionFix("toList()")
            } else {
                RemoveUselessCallFix()
            }
            val descriptor = holder.manager.createProblemDescriptor(
                expression,
                TextRange(
                    expression.operationTokenNode.startOffset - expression.startOffset,
                    calleeExpression.endOffset - expression.startOffset
                ),
                KotlinBundle.message("useless.call.on.collection.type"),
                ProblemHighlightType.LIKE_UNUSED_SYMBOL,
                isOnTheFly,
                fix
            )
            holder.registerProblem(descriptor)
        }
    }

    private fun KotlinType?.isList() = this?.constructor?.declarationDescriptor?.fqNameSafe == StandardNames.FqNames.list
}
